package com.flow.framework.base.service.access.log.impl;

import com.flow.framework.base.properties.FrameworkBaseConfigProperties;
import com.flow.framework.base.properties.component.RequestConfigProperties;
import com.flow.framework.base.properties.component.ResponseConfigProperties;
import com.flow.framework.base.service.access.log.IAccessLogService;
import com.flow.framework.common.constant.FrameworkCommonConstant;
import com.flow.framework.common.error.SystemErrorCode;
import com.flow.framework.common.exception.CheckedException;
import com.flow.framework.common.json.JsonObject;
import com.flow.framework.common.util.verify.VerifyUtil;
import com.flow.framework.core.service.properties.ISystemConfigPropertiesService;
import com.flow.framework.core.system.initialization.ApplicationContextHelper;
import com.flow.framework.core.system.listener.lifecycle.ISystemLifecycleListener;
import com.flow.framework.core.system.thread.pool.executor.ThreadPoolExecutor;
import com.flow.framework.core.system.thread.pool.policy.RejectedPolicy;
import com.flow.framework.core.system.thread.pool.task.BaseRunnable;
import com.flow.framework.facade.access.log.module.service.IAccessLogFrameworkModuleService;
import com.flow.framework.facade.access.log.opt.annotation.OptLog;
import com.flow.framework.facade.access.log.pojo.dto.OptLogModuleDto;
import com.flow.framework.facade.access.log.pojo.dto.TraceLogModuleDto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.*;

import java.lang.reflect.Method;
import java.util.*;

/**
 * 请求日志记录服务
 *
 * @author luoguopiao
 * @version 0.0.1
 * @date 2022/12/11
 */
@Slf4j
@RequiredArgsConstructor
public class AccessLogServiceImpl implements IAccessLogService, ISystemLifecycleListener {

    private static final String PATH_SEPARATOR = "/";

    private static final ThreadPoolExecutor LOG_THREAD_POOL =
            new ThreadPoolExecutor(1, 4, 5 * 60 * 1000,
                    10240, "record-access-log-thread",
                    RejectedPolicy.DISCARD_POLICY);

    private static final long LOG_TASK_TIMEOUT = 10 * 1000;

    private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    private final Map<String, Map<String, OptLog>> URI_METHOD_OPT_LOG_MAP = new HashMap<>();

    private final IAccessLogFrameworkModuleService accessLogFrameworkModuleService;

    private final FrameworkBaseConfigProperties frameworkBaseConfigProperties;

    private final ISystemConfigPropertiesService systemConfigPropertiesService;

    private String contextPath = FrameworkCommonConstant.EMPTY_STRING;

    /**
     * @inheritDoc
     */
    @Nullable
    @Override
    public OptLog matchOptLogAnnotation(String method, String uri) {
        Set<Map.Entry<String, Map<String, OptLog>>> entries = URI_METHOD_OPT_LOG_MAP.entrySet();
        for (Map.Entry<String, Map<String, OptLog>> entry : entries) {
            String protoUri = entry.getKey();
            if (PATH_MATCHER.match(protoUri, uri)) {
                Map<String, OptLog> methodOptLogMap = entry.getValue();
                if (!VerifyUtil.isEmpty(methodOptLogMap)) {
                    return methodOptLogMap.get(method);
                } else {
                    return null;
                }
            }
        }
        return null;
    }

    /**
     * @inheritDoc
     */
    @Override
    public void asyncRecordOptLog(OptLogModuleDto optLogModuleDto) {
        LOG_THREAD_POOL.submit(new BaseRunnable(LOG_TASK_TIMEOUT) {
            @Override
            protected void execute() {
                recordTraceLog(optLogModuleDto.getTraceLogModuleDto());
                accessLogFrameworkModuleService.recordOptLog(optLogModuleDto);
            }
        });
    }

    /**
     * @inheritDoc
     */
    @Override
    public void onStartUp() {
        contextPath = systemConfigPropertiesService.getConfigValue("server.servlet.context-path",
                FrameworkCommonConstant.EMPTY_STRING);
        Collection<Object> restControllers = ApplicationContextHelper.getBeansWithAnnotation(RestController.class);
        Collection<Object> traditionalControllers = ApplicationContextHelper.getBeansWithAnnotation(Controller.class);
        Collection<Object> requestMappings = ApplicationContextHelper.getBeansWithAnnotation(RequestMapping.class);
        Set<Object> controllers = new HashSet<>();
        controllers.addAll(restControllers);
        controllers.addAll(traditionalControllers);
        controllers.addAll(requestMappings);
        for (Object controller : controllers) {
            Class<?> controllerClazz = controller.getClass();
            Method[] declaredMethods = controllerClazz.getDeclaredMethods();
            for (Method method : declaredMethods) {
                boolean isPresent = method.isAnnotationPresent(OptLog.class);
                if (!isPresent) {
                    continue;
                }
                OptLog optLog = method.getDeclaredAnnotation(OptLog.class);
                List<MappingDefine> mappingDefines = getMappingDefines(method, controllerClazz);
                for (MappingDefine mappingDefine : mappingDefines) {
                    String path = mappingDefine.getPath();
                    Map<String, OptLog> methodAndOptLog = URI_METHOD_OPT_LOG_MAP
                            .computeIfAbsent(path, (key) -> new HashMap<>(16));
                    methodAndOptLog.put(mappingDefine.getMethod(), optLog);
                }
            }
        }

    }

    /**
     * @inheritDoc
     */
    @Override
    public void asyncRecordTraceLog(TraceLogModuleDto traceLogModuleDto) {
        final RequestConfigProperties requestConfigProperties = frameworkBaseConfigProperties.getRequest();
        LOG_THREAD_POOL.submit(new BaseRunnable(LOG_TASK_TIMEOUT) {
            @Override
            protected void execute() {
                recordTraceLog(traceLogModuleDto);
                if (requestConfigProperties.isEnableRecordPersistence()) {
                    accessLogFrameworkModuleService.recordTraceLog(traceLogModuleDto);
                }
            }
        });
    }

    private void recordTraceLog(TraceLogModuleDto traceLogModuleDto) {
        final RequestConfigProperties requestConfigProperties = frameworkBaseConfigProperties.getRequest();
        final ResponseConfigProperties responseConfigProperties = frameworkBaseConfigProperties.getResponse();
        String requestBody = traceLogModuleDto.getRequestBody();
        String responseBody = traceLogModuleDto.getResponseBody();
        if (requestConfigProperties.isEnableRecordLocal()) {
            int requestRecordLocalMaxLength = requestConfigProperties.getRecordLocalMaxLength();
            if (requestBody.length() > requestRecordLocalMaxLength) {
                traceLogModuleDto.setRequestBody(requestBody.substring(0, requestRecordLocalMaxLength));
            }
            int responseRecordLocalMaxLength = responseConfigProperties.getRecordLocalMaxLength();
            if (responseBody.length() > responseRecordLocalMaxLength) {
                traceLogModuleDto.setResponseBody(responseBody.substring(0, responseRecordLocalMaxLength));
            }
            log.info("request log : {}", JsonObject.toString(traceLogModuleDto));
        }

        if (requestConfigProperties.isEnableRecordPersistence()) {
            int requestRecordPersistenceMaxLength = requestConfigProperties.getRecordPersistenceMaxLength();
            if (requestBody.length() > requestRecordPersistenceMaxLength) {
                traceLogModuleDto.setRequestBody(requestBody.substring(0, requestRecordPersistenceMaxLength));
            } else {
                traceLogModuleDto.setRequestBody(requestBody);
            }
            int responseRecordPersistenceMaxLength = responseConfigProperties.getRecordPersistenceMaxLength();
            if (responseBody.length() > responseRecordPersistenceMaxLength) {
                traceLogModuleDto.setResponseBody(responseBody.substring(0, responseRecordPersistenceMaxLength));
            } else {
                traceLogModuleDto.setResponseBody(responseBody);
            }
        }
    }

    /**
     * 获取controller的mapping信息
     *
     * @param method          method
     * @param controllerClazz controllerClazz
     * @return
     */
    private List<MappingDefine> getMappingDefines(Method method, Class controllerClazz) {
        String[] controllerPaths = null;
        boolean annotationPresent = controllerClazz.isAnnotationPresent(RequestMapping.class);
        if (annotationPresent) {
            RequestMapping annotation = (RequestMapping) controllerClazz.getDeclaredAnnotation(RequestMapping.class);
            controllerPaths = Optional.of(annotation.value()).filter(value -> !VerifyUtil.isEmpty(value)).orElse(annotation.path());
        }

        boolean isGetMapping = method.isAnnotationPresent(GetMapping.class);
        if (isGetMapping) {
            GetMapping declaredAnnotation = method.getDeclaredAnnotation(GetMapping.class);
            return constructMappingDefines(controllerPaths, declaredAnnotation.value(), declaredAnnotation.path(),
                    RequestMethod.GET.name());
        }
        boolean isPostMapping = method.isAnnotationPresent(PostMapping.class);
        if (isPostMapping) {
            PostMapping declaredAnnotation = method.getDeclaredAnnotation(PostMapping.class);
            return constructMappingDefines(controllerPaths, declaredAnnotation.value(), declaredAnnotation.path(),
                    RequestMethod.POST.name());
        }

        boolean isPutMapping = method.isAnnotationPresent(PutMapping.class);
        if (isPutMapping) {
            PutMapping declaredAnnotation = method.getDeclaredAnnotation(PutMapping.class);
            return constructMappingDefines(controllerPaths, declaredAnnotation.value(), declaredAnnotation.path(),
                    RequestMethod.PUT.name());
        }

        boolean isDeleteMapping = method.isAnnotationPresent(DeleteMapping.class);
        if (isDeleteMapping) {
            DeleteMapping declaredAnnotation = method.getDeclaredAnnotation(DeleteMapping.class);
            return constructMappingDefines(controllerPaths, declaredAnnotation.value(), declaredAnnotation.path(),
                    RequestMethod.DELETE.name());
        }

        boolean isPatchMapping = method.isAnnotationPresent(PatchMapping.class);
        if (isPatchMapping) {
            PatchMapping declaredAnnotation = method.getDeclaredAnnotation(PatchMapping.class);
            return constructMappingDefines(controllerPaths, declaredAnnotation.value(), declaredAnnotation.path(),
                    RequestMethod.PATCH.name());
        }

        boolean isRequestMapping = method.isAnnotationPresent(RequestMapping.class);
        if (isRequestMapping) {
            RequestMapping declaredAnnotation = method.getDeclaredAnnotation(RequestMapping.class);
            List<MappingDefine> mappingDefines = new ArrayList<>();
            RequestMethod[] requestMethods = declaredAnnotation.method();
            for (RequestMethod requestMethod : requestMethods) {
                List<MappingDefine> subMappingDefines = constructMappingDefines(controllerPaths, declaredAnnotation.value(), declaredAnnotation.path(),
                        requestMethod.name());
                mappingDefines.addAll(subMappingDefines);
            }
            return mappingDefines;
        }
        log.error("can't find controller mapping.");
        throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
    }

    private List<MappingDefine> constructMappingDefines(String[] controllerPaths, String[] values, String[] paths, String method) {
        List<MappingDefine> mappingDefines = new ArrayList<>();
        String[] targetPaths = Optional.ofNullable(values).filter(value -> !VerifyUtil.isEmpty(value)).orElse(paths);
        if (VerifyUtil.isEmpty(targetPaths)) {
            return mappingDefines;
        }

        if (!VerifyUtil.isEmpty(controllerPaths)) {
            for (String controllerPath : controllerPaths) {
                for (String targetPath : targetPaths) {
                    mappingDefines.add(new MappingDefine(
                            PATH_SEPARATOR + trimPathSeparator(contextPath)
                                    + PATH_SEPARATOR + trimPathSeparator(controllerPath)
                                    + PATH_SEPARATOR + trimPathSeparator(targetPath), method));
                }
            }
        } else {
            for (String targetPath : targetPaths) {
                mappingDefines.add(new MappingDefine(
                        PATH_SEPARATOR + trimPathSeparator(contextPath)
                                + PATH_SEPARATOR + trimPathSeparator(targetPath), method));
            }
        }
        return mappingDefines;
    }

    private String trimPathSeparator(String s) {
        return Optional.ofNullable(s)
                .map(string -> {
                    String finalString = string;
                    if (finalString.startsWith(PATH_SEPARATOR)) {
                        finalString = finalString.substring(1);
                    }
                    if (finalString.endsWith(PATH_SEPARATOR)) {
                        finalString = finalString.substring(0, finalString.length() - 1);
                    }
                    return finalString;
                })
                .orElse(FrameworkCommonConstant.EMPTY_STRING);
    }


    @Data
    @AllArgsConstructor
    private static class MappingDefine {

        private String path;

        private String method;
    }
}
